#!/bin/bash

############################################################################
#
# Script for testing block device I/O performance. Running this script on a
# block device that is connected to a remote SCST target device allows to
# test the performance of the transport protocols implemented in SCST. The
# operation of this script is similar to iozone, while this script is easier
# to use.
#
# Copyright (C) 2009 Bart Van Assche <bvanassche@acm.org>.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation, version 2
# of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
############################################################################

#########################
# Function definitions  #
#########################

usage() {
  echo "Usage: $0 [-a] [-d] [-f] [-i <i>] [-j] [-n] [-r] [-s <l2s>] [-t [user@]host] <dev>"
  echo "        -a - use asynchronous (buffered) I/O."
  echo "        -d - use direct (non-buffered) I/O."
  echo "        -f - force -- skip the test if there is still data present."
  echo "        -i - number times each test is iterated."
  echo "        -j - use fio instead of dd for benchmarking."
  echo "        -m <l2min> - log2 of the smallest block size to use."
  echo "        -M <l2max> - log2 of the largest block size to use."
  echo "        -n - do not verify the data on <dev> before overwriting it."
  echo "        -r - only perform the read test."
  echo "        -s - logarithm base two of the I/O size."
  echo "        -t - username and hostname of the target to drop the caches on."
  echo "        -w - only perform the write test."
  echo "        <dev> - block device to run the I/O performance test on."
}

# Compute two raised to the power $1.
pow2() {
  if [ $1 = 0 ]; then
    echo 1
  else
    echo $((2 * $(pow2 $(($1 - 1)) ) ))
  fi
}

# Report via the exit status whether or not the current user has sufficient
# privileges to drop the VM caches.
can_drop_cache() {
  [ -w /proc/sys/vm/drop_caches ]
}

drop_caches() {
  sync
  if can_drop_cache; then
    echo 3 > /proc/sys/vm/drop_caches
  fi
  if [ "${target_login}" != "" ]; then
    ssh -n ${target_login} 'sync; if [ -w /proc/sys/vm/drop_caches ]; then echo 3 > /proc/sys/vm/drop_caches; fi'
  fi
}

# Set the scaling governor, minimum and maximum frequency to $1, $2 and $3
# respectively or disable frequency scaling when no arguments have been
# provided.
set_frequency_scaling() {
  local syscpu=/sys/devices/system/cpu
  if [ ! -e $syscpu/cpufreq ]; then
    return
  fi
  local governor=$(cat $syscpu/cpu0/cpufreq/scaling_governor)
  local cpuinfo_min_freq=$(cat $syscpu/cpu0/cpufreq/cpuinfo_min_freq)
  local cpuinfo_max_freq=$(cat $syscpu/cpu0/cpufreq/cpuinfo_max_freq)
  local scaling_min_freq=$(cat $syscpu/cpu0/cpufreq/scaling_min_freq)
  local scaling_max_freq=$(cat $syscpu/cpu0/cpufreq/scaling_max_freq)
  if [ -w $syscpu/cpu0/cpufreq/scaling_governor ]; then
    for d in $syscpu/cpu*/cpufreq
    do
      echo "${1:-userspace}"         >"$d/scaling_governor"
      echo "${2:-$cpuinfo_max_freq}" >"$d/scaling_min_freq"
      echo "${3:-$cpuinfo_max_freq}" >"$d/scaling_max_freq"
    done
  fi
  echo $governor $scaling_min_freq $scaling_max_freq
}

# Read times in seconds from stdin, one number per line, echo each number
# using format $1, and also echo the average transfer size in MB/s, its
# standard deviation and the number of IOPS using the total I/O size $2 and
# the block transfer size $3.
echo_and_calc_avg() {
  awk -v fmt="$1" -v iosize="$2" -v blocksize="$3" 'BEGIN{pow_2_20=1024*1024}{if ($1 != 0){n++;sum+=iosize/$1;sumsq+=iosize*iosize/($1*$1)};printf fmt, $1} END{d=(n>0?sumsq/n-sum*sum/n/n:0);avg=(n>0?sum/n:0);stddev=(d>0?sqrt(d):0);iops=avg/blocksize;printf fmt fmt fmt,avg/pow_2_20,stddev/pow_2_20,iops}'
}

time_write() {
  if [ "${use_fio}" = "true" ]; then
    if [ "${iotype}" = "direct" ]; then
      fio_flags="--direct=1"
    else
      fio_flags="--direct=0 --end_fsync=1"
    fi
    fio  --rw=write --filename="${device}" --bs=$1 --size=$(($1*$2)) --ioengine=psync --end_fsync=1 --invalidate=1 ${fio_flags} --name=writeperftest \
    | sed -n 's/.*runt= *\([0-9]*\)msec.*/\1/p' \
    | awk '{print $1/1000}'
  else
    drop_caches
    if [ "${iotype}" = "direct" ]; then
      dd_oflags="oflag=direct conv=notrunc"
    else
      dd_oflags=""
    fi
    { dd if=/dev/zero of="${device}" bs=$1 count=$2 ${dd_oflags} 2>&1; sync; } \
      | sed -n -e 's/.* \([0-9.]*\) s[econds]*,.*/\1/p' | sed 's/^$/0/'
  fi
}

time_read() {
  if [ "${use_fio}" = "true" ]; then
    if [ "${iotype}" = "direct" ]; then
      fio_flags="--direct=1"
    else
      fio_flags="--direct=0"
    fi
    fio  --rw=read --filename="${device}" --bs=$1 --size=$(($1*$2)) --ioengine=psync --end_fsync=1 --invalidate=1 ${fio_flags} --name=readperftest \
    | sed -n 's/.*runt= *\([0-9]*\)msec.*/\1/p' \
    | awk '{print $1/1000}'
  else
    drop_caches
    if [ "${iotype}" = "direct" ]; then
      dd_iflags="iflag=direct"
    else
      dd_iflags=""
    fi
    dd if="${device}" of=/dev/null bs=$1 count=$2 ${dd_iflags} 2>&1 \
      | sed -n -e 's/.* \([0-9.]*\) s[econds]*,.*/\1/p' | sed 's/^$/0/'
  fi
}


#########################
# Default settings      #
#########################

force=false
iterations=3
log2_io_size=30       # 1 GB
log2_min_blocksize=9  # 512 bytes
log2_max_blocksize=26 # 64 MB
iotype=direct
perform_read_test=true
perform_write_test=true
target_login=""
use_fio=false
verify_device_data=true


#########################
# Argument processing   #
#########################

set -- $(/usr/bin/getopt "adfhi:jm:M:nrs:t:w" "$@")
while [ "$1" != "${1#-}" ]
do
  case "$1" in
    '-a') iotype="buffered"; shift;;
    '-d') iotype="direct"; shift;;
    '-f') force="true"; shift;;
    '-i') iterations="$2"; shift; shift;;
    '-j') use_fio=true; shift;;
    '-m') log2_min_blocksize="$2"; shift; shift;;
    '-M') log2_max_blocksize="$2"; shift; shift;;
    '-n') verify_device_data="false"; shift;;
    '-r') perform_write_test="false"; shift;;
    '-s') log2_io_size="$2"; shift; shift;;
    '-t') target_login="$2"; shift; shift;;
    '-w') perform_read_test="false"; shift;;
    '--') shift;;
    *)    usage; exit 1;;
  esac
done

if [ "$#" != 1 ]; then
  usage
  exit 1
fi

device="$1"


####################
# Performance test #
####################

if [ ! -e "${device}" ]; then
  echo "Error: device ${device} does not exist."
  exit 1
fi

if [ "${perform_write_test}" = "true" -a ! -w "${device}" ]; then
  echo "Error: device ${device} is not writeable."
  exit 1
fi

if [ "${perform_read_test}" = "true" -a                              \
     "$(($(cat /sys/class/block/$(basename $device)/size) * 512))" -lt $(pow2 $log2_io_size) ]
then
  echo "Error: device ${device} contains less than $(pow2 $log2_io_size) bytes."
  exit 1
fi

if [ "${perform_write_test}" = "true" -a "${verify_device_data}" = "true" ] \
   && [ "${force}" != "true" ] \
   && ! cmp -s -n $(pow2 $log2_io_size) "${device}" /dev/zero
then
  echo "Error: device ${device} still contains data."
  exit 1
fi

if ! can_drop_cache; then
  echo ""
  echo "WARNING: insufficient privileges to drop the file system cache"
  echo "-- results will be unreliable."
  echo ""
fi

# Disable frequency scaling
if [ -e /sys/devices/system/cpu/cpu0/cpufreq ]; then
  if [ -w /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor ]; then
    frequency_scaling_params="$(set_frequency_scaling)"
  else
    echo ""
    echo "WARNING: insufficient privileges to disable CPU frequency scaling"
    echo "-- results will be unreliable."
    echo ""
  fi
fi

# Header, line 1
printf "%9s " blocksize
i=0
while [ $i -lt ${iterations} ]
do
  printf "%8s " "W"
  i=$((i+1))
done
printf "%8s %8s %8s " "W(avg," "W(std," "W"
i=0
while [ $i -lt ${iterations} ]
do
  printf "%8s " "R"
  i=$((i+1))
done
printf "%8s %8s %8s" "R(avg," "R(std," "R"
printf "\n"

# Header, line 2
printf "%9s " "(bytes)"
i=0
while [ $i -lt ${iterations} ]
do
  printf "%8s " "(s)"
  i=$((i+1))
done
printf "%8s %8s %8s " "MB/s)" "MB/s)" "(IOPS)"
i=0
while [ $i -lt ${iterations} ]
do
  printf "%8s " "(s)"
  i=$((i+1))
done
printf "%8s %8s %8s" "MB/s)" "MB/s)" "(IOPS)"
printf "\n"

# Measurements
log2_blocksize=${log2_max_blocksize}
while [ ! $log2_blocksize -lt $log2_min_blocksize ]
do
  if [ $log2_blocksize -gt $log2_io_size ]; then
    log2_blocksize=$((log2_blocksize - 1))
    continue
  fi
  iosize=$(pow2 $log2_io_size)
  bs=$(pow2 $log2_blocksize)
  count=$(pow2 $(($log2_io_size - $log2_blocksize)))
  printf "%9d " ${bs}
  i=0
  while [ $i -lt ${iterations} ]
  do
    if [ "${perform_write_test}" = "true" ]; then
      time_write ${bs} ${count}
    else
      echo " 0 s,"
    fi
    i=$((i+1))
  done | echo_and_calc_avg "%8.3f " ${iosize} ${bs}

  i=0
  while [ $i -lt ${iterations} ]
  do
    if [ "${perform_read_test}" = "true" ]; then
      time_read ${bs} ${count}
    else
      echo " 0 s,"
    fi
    i=$((i+1))
  done | echo_and_calc_avg "%8.3f " ${iosize} ${bs}
  printf "\n"
  log2_blocksize=$((log2_blocksize - 1))
done

# Restore frequency scaling
set_frequency_scaling ${frequency_scaling_params} >/dev/null
